﻿using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;

using Adaptive.Agrona;
using Adaptive.Agrona.Concurrent;
using Adaptive.Agrona.Concurrent.RingBuffer;
using Adaptive.Agrona.Util;
using Microsoft.Extensions.Logging;

using Unity.Commons.Client.Exceptions;
using Unity.Commons.Client.Generator;
using Unity.Logger;
using Unity.Type;
using Unity.Type.BinaryType;

namespace Unity.Commons.Client.Buffer
{
    public class UnityBufferFile : IDisposable
    {
        private static readonly ILogger Logger = UnityLogger.ConsoleLoggerFactory.CreateLogger(nameof(UnityBufferFile));

        static readonly byte[] Signature = new byte[] { 85, 78, 73, 84, 89, 66, 85, 70 };

        static ImmutableList<FileInfo> CurrentUsingFiles = ImmutableList.Create<FileInfo>();

        static readonly int MetadataTimestampBytes = 8;
        static readonly int MetadataBufferSizeBytes = 4;
        static readonly int MetadataBufferShiftBytes = 4;
        static readonly int MetadataFormatBytes = 4;
        static readonly int MetadataRecordSizeBytes = 4;

        static readonly int MetadataSize =
            MetadataTimestampBytes + MetadataBufferSizeBytes + MetadataBufferShiftBytes
            + MetadataFormatBytes + MetadataRecordSizeBytes;

        static readonly int HeaderSectionSize = 128;
        static readonly int FormatVersion = 1;

        private readonly FileInfo _bufferFileInfo;
        private ICollection<byte[]> _recoveryData = new List<byte[]>();
        private static readonly ICollection<byte[]> RecoveryBuffer = new List<byte[]>();

        private long _timestamp;
        private int _bufferSize;
        private int _bufferShift;
        private int _format;
        private int _recordSize;

        public long Timestamp => _timestamp;

        public int BufferSize => _bufferSize;

        public int BufferShift => _bufferShift;

        public int Format => _format;

        public int RecordSize => _recordSize;

        public FileInfo BufferFileInfo => _bufferFileInfo;

        public ICollection<byte[]> Recovery => _recoveryData;

        static ICollection<byte[]> BackupPreviousBufferFile(FileInfo bufferFile)
        {
            if (bufferFile == null)
            {
                const string exceptionMessage = "Exception: Can't processing null pointer to backup file";
                Logger.LogError(exceptionMessage);
                throw new UnityBufferFileException(exceptionMessage);
            }
            if (bufferFile.Exists)
            {
                if (IsValudFileType(bufferFile))
                {
                    try
                    {
                        FileInfo backupFile = BackupBuffer(bufferFile);
                        return VerifyFileContent(backupFile);
                    }
                    catch (UnityBufferFileException exception)
                    {
                        const string exceptionMessage = "Exception: Can't recovery data from buffer file";
                        Logger.LogError(exceptionMessage);
                        throw new UnityBufferFileException(exceptionMessage, exception);
                    }
                }
                else
                {
                    try
                    {
                        long fileSize = bufferFile.Length;
                        if (fileSize > 0)
                        {
                            const string exceptionMessage = "Exception: Exist backup file overwrite data error";
                            Logger.LogError(exceptionMessage);
                            throw new UnityBufferFileException(exceptionMessage);
                        }
                    }
                    catch (Exception exception)
                    {
                        const string exceptionMessage = "Exception: Exists backup file access error";
                        Logger.LogError(exceptionMessage);
                        throw new UnityBufferFileException(exceptionMessage, exception);
                    }
                }
            }
            return new List<byte[]>();
        }

        public static UnityBufferFile MakeNewBufferFile(string filepath, int bufferSize)
        {
            if (string.IsNullOrWhiteSpace(filepath))
            {
                throw new UnityBufferFileException();
            }
            if (bufferSize <= 0)
            {
                throw new UnityBufferFileException();
            }
            FileInfo bufferFile = new FileInfo(filepath);
            if (CurrentUsingFiles.Contains(bufferFile))
            {
                throw new UnityBufferFileException();
            }

            ICollection<byte[]> recoveryContent = BackupPreviousBufferFile(bufferFile);
            try
            {
                using (FileStream stream = new FileStream(filepath, FileMode.CreateNew))
                {
                    using (BinaryWriter writer = new BinaryWriter(stream))
                    {
                        WriteSignature(writer);
                        WriteMetadata(writer, bufferSize);
                        AllocateBuffer(writer, HeaderSectionSize, bufferSize);
                    }
                }
            }
            catch (Exception exception)
            {
                throw new UnityBufferFileException("Unexpected exception when make new backup buffer", exception);
            }

            UnityBufferFile returnBufferFile = new UnityBufferFile(filepath);
            returnBufferFile._recoveryData = new List<byte[]>(recoveryContent);
            return returnBufferFile;
        }

        static bool Equality(byte[] left, byte[] right)
        {
            int index;
            if (left.Length == right.Length)
            {
                index = 0;
                while (index < left.Length && (left[index] == right[index])) { index++; }
                if (index == left.Length) return true;
            }
            return false;
        }

        public UnityBufferFile(string filepath)
        {
            if (string.IsNullOrWhiteSpace(filepath))
            {
                throw new UnityBufferFileException();
            }
            _bufferFileInfo = new FileInfo(filepath);
            if (!_bufferFileInfo.Exists)
            {
                throw new UnityBufferFileException();
            }
            if (!File.Exists(filepath))
            {
                throw new UnityBufferFileException();
            }
            if (CurrentUsingFiles.Contains(_bufferFileInfo))
            {
                throw new UnityBufferFileException();
            }

            CurrentUsingFiles = CurrentUsingFiles.Add(_bufferFileInfo);

            try
            {
                FileStream stream = new FileStream(filepath, FileMode.Open);
                BinaryReader reader = new BinaryReader(stream);
                byte[] signature = ReadSignature(reader);
                if (Equality(signature, Signature))
                {
                    ReadMetadata(reader);
                }
                else
                {
                    throw new UnityBufferFileException();
                }
                reader.Close();
                stream.Close();
            }
            catch (Exception exception)
            {
                throw new UnityBufferFileException("Unexpected exception when initialize new buffer file", exception);
            }
        }

        static FileInfo BackupBuffer(FileInfo fileInfo)
        {
            if (fileInfo == null)
            {
                throw new UnityBufferFileException();
            }
            FileInfo to = new FileInfo(fileInfo.FullName + ".bkp");
            try
            {
                fileInfo.CopyTo(to.FullName, true);
            }
            catch (Exception exception)
            {
                throw new UnityBufferFileException("Unexpected exception in method backup buffer", exception);
            }
            return to;
        }

        static void AllocateBuffer(BinaryWriter writer, int bufferShift, int bufferSize)
        {
            if (writer == null)
            {
                throw new UnityBufferFileException();
            }
            if (bufferShift < HeaderSectionSize)
            {
                throw new UnityBufferFileException();
            }
            if (bufferSize <= 0)
            {
                throw new UnityBufferFileException();
            }
            writer.Write(new byte[HeaderSectionSize - MetadataSize - Signature.Length]);
            writer.Write(new byte[bufferSize]);
        }

        static bool IsValudFileType(FileInfo fileInfo)
        {
            try
            {
                using (FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open))
                {
                    using (BinaryReader reader = new BinaryReader(stream))
                    {
                        return Equality(Signature, ReadSignature(reader));
                    }
                    
                }
            }
            catch (Exception exception)
            {
                // log exception
                return false;
            }
        }

        private static byte[] ReadSignature(BinaryReader reader)
        {
            if (reader == null)
            {
                throw new UnityBufferFileException();
            }
            byte[] signature = reader.ReadBytes(Signature.Length);
            if (!Equality(signature, Signature))
            {
                // log error
            }
            return signature;
        }

        private static void WriteSignature(BinaryWriter writer)
        {
            if(writer == null)
            {
                throw new UnityBufferFileException();
            }
            writer.Write(Signature);
        }

        private void ReadMetadata(BinaryReader reader)
        {
            if (reader == null)
            {
                throw new UnityBufferFileException();
            }
            _timestamp = reader.ReadInt64();
            _bufferSize = reader.ReadInt32();
            _bufferShift = reader.ReadInt32();
            _format = reader.ReadInt32();
            _recordSize = reader.ReadInt32();
        }

        static void WriteMetadata(BinaryWriter writer, int bufferSize)
        {
            if (writer == null)
            {
                throw new UnityBufferFileException();
            }
            long unixTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            writer.Write(unixTimestamp);
            writer.Write(bufferSize);
            writer.Write(HeaderSectionSize);
            writer.Write(FormatVersion);
            writer.Write(UnityGenerator.MessageLength);
        }

        public IAtomicBuffer TryMakeBufferFile()
        {
            MappedByteBuffer mappedByteBuffer = IoUtil.MapExistingFile(_bufferFileInfo.FullName, MapMode.ReadWrite);
            return new UnsafeBuffer(mappedByteBuffer);
        }

        static ICollection<byte[]> VerifyFileContent(FileInfo fileInfo)
        {
            if (fileInfo == null)
            {
                throw new UnityBufferFileException();
            }
            RecoveryBuffer.Clear();
            try
            {
                UnityBufferFile bufferFile = new UnityBufferFile(fileInfo.FullName);
                MappedByteBuffer mappedByteBuffer = IoUtil.MapExistingFile(fileInfo, bufferFile.BufferShift, bufferFile.BufferSize);
                UnsafeBuffer unsafeBuffer = new UnsafeBuffer(mappedByteBuffer);
                IRingBuffer ringBuffer = new ManyToOneRingBuffer(unsafeBuffer);
                ringBuffer.Read(RingBufferRecoveryHandler);
                bufferFile.Close();
            }
            catch (Exception exception)
            {
                // log
            }
            return RecoveryBuffer;
        }

        private static readonly MetadataDeserializer Deserializer = new MetadataDeserializer();

        static void RingBufferRecoveryHandler(int msgTypeId, IMutableDirectBuffer buffer, int index, int length)
        {
            byte[] message = new byte[length];
            buffer.GetBytes(index, message);

            Metadata metadata = Deserializer.Deserialize(message);
            if (metadata != null)
            {
                RecoveryBuffer.Add(message);
            }
            else
            {
                // logger 
            }
        }

        public void Close()
        {
            CurrentUsingFiles = CurrentUsingFiles.Remove(_bufferFileInfo);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            Close();
        }
    }
}
